Repository navigation
ast: record the brace on a hash literal (HashLit.Braced) - #42
Merged
Merged
Conversation
`f({k: 1})` and `f(k: 1)` built structurally identical `ast.HashLit`s, so
nothing downstream could tell a POSITIONAL Hash argument from KEYWORDS.
`ast.Call` carries no position either, so a consumer could not re-read the
source to recover the brace.
Add one bit of written form, following MRI, which records exactly this bit
in exactly this place:
* `primary: tLBRACE assoc_list '}'` sets `RNODE_HASH($$)->nd_brace = TRUE`
(parse.y-ruby_4_0:4415-4419);
* `rb_node_hash_new` leaves it 0 for every hash the grammar assembles
itself (parse.y-ruby_4_0:11784-11791);
* `aryset_check` reads `!nd_brace` back to mean "keywords"
(parse.y-ruby_4_0:13669).
So `parseHashLiteral` sets Braced and `addKwPair` does not. The bit says
only "written with braces" and is deliberately not conditional on being an
argument, again as in MRI: `x = {a: 1}` is Braced too. A consumer asking
"positional Hash argument?" asks Braced of a node it already found in
Call.Args.
`obj.m {}`, where the braces are a BLOCK, cannot be mislabelled: a `{`
after a call never reaches parseHashLiteral, structurally the same defence
as MRI's lexer, which picks tLBRACE (hash) or '{' / tLBRACE_ARG (block)
before the grammar sees it (parse.y-ruby_4_0:11141-11153). There is no
HashLit there to mislabel, and a test asserts it.
The zero value is false, so no existing consumer changes. No existing test
was modified; the full suite passes unchanged.
Closes #40
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #40.
The gap
f({k: 1})andf(k: 1)built structurally identicalast.HashLits. In Ruby 3+ they are different calls — a positional Hash and keywords — andast.Callcarries no position, so a consumer could not re-read the source to recover the brace either. Reproduction from the issue, against ruby 4.0.5:Only the braced literal at a call site diverges, which is what a missing bit predicts.
The grammar says to record it here
MRI carries this exact bit, in this exact place, and I followed it rather than inventing a shape:
parse.y-ruby_4_0primary : tLBRACE assoc_list '}'RNODE_HASH($$)->nd_brace = TRUErb_node_hash_newnd_brace = 0for every hash the grammar assembles itselfaryset_check!nd_braceback to mean "these are keywords"parser_yylex,case '{'tLBRACE(hash) vs'{'/tLBRACE_ARG(block) in the lexerassoc : tDSTAR arg_value**h, reached from inside braces and from a bare argument list alikeSo
parseHashLiteralsetsBracedandaddKwPairdoes not — one line each, mirroring MRI one to one.The bit says only "written with braces", and is deliberately not conditional on being an argument, again as in MRI:
x = {a: 1}isBracedtoo. A consumer asking "positional Hash argument?" asksBracedof a node it already found inCall.Args. Making the parser guess the call-site condition would put the consumer's question into the parser.The bit takes both values
Measured by the tests added here.
Bracedis neither always true nor always false, and the two spellings that must differ do differ:Bracedf({k: 1})f(k: 1)Keys/Valuesf({})f()f(h)f({**h})f(**h)assoc: tDSTARproduction, different formf(k: 1, **h)f({1 => 2})f(*a, {k: 1})f(a, k: 1)[1, {k: 2}][1, k: 2]x = {a: 1}obj.m {}obj.m { |x| x }obj.m({k: 1}) { |y| y }obj.m {}is where a careless rule breaks the language, and ours cannot: a{after a call never reachesparseHashLiteral, structurally the same defence as MRI's lexer deciding before the grammar sees it. There is noHashLitthere to mislabel, and a test asserts that.Negative control: with
Braced: truereverted to&ast.HashLit{}, 11 assertions in the new tests fail. The tests measure the change rather than passing alongside it.Existing consumers are unaffected
git diff --name-only origin/mainisast/ast.goandparser.goonly; the one new file ishash_braced_test.go.parser,ast,lexer,token),go vet ./...clean,gofmt -lclean on both changed files.gofmt -l .does reportparen_singleton_def_test.go— that is pre-existing onmain(verified by formatting a pristine copy oforigin/main:paren_singleton_def_test.go) and untouched here, so I left it rather than pad this diff.The zero value is
false, so anyone who does not read the field sees nothing.Why this is a separate PR from #41
The two changes share no code: different structs, disjoint parser sites, no common helper. They are the same idea — one bit of written form, with
RangeLit.Exclusiveas precedent — but nothing would be reused by merging them, and split they can be reviewed and merged independently. See #41's PR for the other half.What the consumer will need (separately)
Not in this PR.
go-embedded-ruby/rubyneeds a released parser version first, and a sibling wave is working there. When it picks this up: at a call site, a trailing*ast.HashLitargument withBraced == trueis a positional argument and must not be spliced into keywords; onlyBraced == falseis the keyword hash.f({})must pass an empty positional Hash wheref()passes nothing.🤖 Generated with Claude Code